package me.minetsh.imaging.crop;


import io.reactivex.Completable;
import io.reactivex.CompletableEmitter;
import io.reactivex.CompletableOnSubscribe;
import io.reactivex.Single;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import me.minetsh.imaging.crop.animation.SimpleValueAnimator;
import me.minetsh.imaging.crop.animation.SimpleValueAnimatorListener;
import me.minetsh.imaging.crop.animation.ValueAnimatorV14;
import me.minetsh.imaging.crop.callback.Callback;
import me.minetsh.imaging.crop.callback.CropCallback;
import me.minetsh.imaging.crop.callback.LoadCallback;
import me.minetsh.imaging.crop.callback.SaveCallback;
import me.minetsh.imaging.crop.util.Logger;
import me.minetsh.imaging.crop.util.Utils;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.DataAbilityRemoteException;
import ohos.agp.animation.Animator;
import ohos.agp.components.AttrHelper;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.Image;
import ohos.agp.components.element.Element;
import ohos.agp.render.*;
import ohos.agp.utils.*;
import ohos.app.Context;
import ohos.data.rdb.ValuesBucket;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.media.image.ImagePacker;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import ohos.media.photokit.metadata.AVStorage;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.net.Uri;

import java.io.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

@SuppressWarnings("unused")
public class CropImageView extends Image implements Component.EstimateSizeListener
        , Component.DrawTask, Component.BindStateChangedListener, Component.TouchEventListener {
    private static final String TAG = CropImageView.class.getSimpleName();

    private static final int HANDLE_SIZE_IN_DP = 14;
    private static final int MIN_FRAME_SIZE_IN_DP = 50;
    private static final int FRAME_STROKE_WEIGHT_IN_DP = 1;
    private static final int GUIDE_STROKE_WEIGHT_IN_DP = 1;
    private static final float DEFAULT_INITIAL_FRAME_SCALE = 1f;
    private static final int DEFAULT_ANIMATION_DURATION_MILLIS = 100;
    private static final int DEBUG_TEXT_SIZE_IN_DP = 15;

    private static final int TRANSPARENT = 0x00000000;
    private static final int TRANSLUCENT_WHITE = 0xBBFFFFFF;
    private static final int WHITE = 0xFFFFFFFF;
    private static final int TRANSLUCENT_BLACK = 0xBB000000;

    private int mViewWidth = 0;
    private int mViewHeight = 0;
    private float mScale = 1.0f;
    private float mAngle = 0.0f;
    private float mImgWidth = 0.0f;
    private float mImgHeight = 0.0f;

    private boolean mIsInitialized = false;
    private Matrix mMatrix = null;
    private Paint mPaintTranslucent;
    private Paint mPaintFrame;
    private Paint mPaintBitmap;
    private Paint mPaintDebug;
    private RectFloat mFrameRect;
    private RectFloat mInitialFrameRect;
    private RectFloat mImageRect;
    private Point mCenter = new Point();
    private float mLastX, mLastY;
    private boolean mIsRotating = false;
    private boolean mIsAnimating = false;
    private SimpleValueAnimator mAnimator = null;
    private final int DEFAULT_INTERPOLATOR = Animator.CurveType.DECELERATE;
    private int mInterpolator = DEFAULT_INTERPOLATOR;
    private EventRunner eventRunner = EventRunner.getMainEventRunner();
    private EventHandler mHandler = new EventHandler(eventRunner);
    private Uri mSourceUri = null;
    private Uri mSaveUri = null;
    private int mExifRotation = 0;
    private int mOutputMaxWidth;
    private int mOutputMaxHeight;
    private int mOutputWidth = 0;
    private int mOutputHeight = 0;
    private boolean mIsDebug = false;
    private String mCompressFormat = "jpeg";
    private int mCompressQuality = 100;
    private int mInputImageWidth = 0;
    private int mInputImageHeight = 0;
    private int mOutputImageWidth = 0;
    private int mOutputImageHeight = 0;
    private AtomicBoolean mIsLoading = new AtomicBoolean(false);
    private AtomicBoolean mIsCropping = new AtomicBoolean(false);
    private AtomicBoolean mIsSaving = new AtomicBoolean(false);
    private ExecutorService mExecutor;
    private int rotateDegrees;
    private TouchArea mTouchArea = TouchArea.OUT_OF_BOUNDS;

    private CropMode mCropMode = CropMode.SQUARE;
    private ShowMode mGuideShowMode = ShowMode.SHOW_ALWAYS;
    private ShowMode mHandleShowMode = ShowMode.SHOW_ALWAYS;
    private float mMinFrameSize;
    private int mHandleSize;
    private int mTouchPadding = 0;
    private boolean mShowGuide = true;
    private boolean mShowHandle = true;
    private boolean mIsCropEnabled = true;
    private boolean mIsEnabled = true;
    private Point mCustomRatio = new Point(1.0f, 1.0f);
    private float mFrameStrokeWeight = 2.0f;
    private float mGuideStrokeWeight = 2.0f;
    private int mBackgroundColor;
    private int mOverlayColor;
    private int mFrameColor;
    private int mHandleColor;
    private int mGuideColor;
    private float mInitialFrameScale; // 0.01 ~ 1.0, 0.75 is default value
    private boolean mIsAnimationEnabled = true;
    private int mAnimationDurationMillis = DEFAULT_ANIMATION_DURATION_MILLIS;
    private boolean mIsHandleShadowEnabled = true;
    private int moreLength = 20;
    private boolean isMove = false;

    /**
     * 构造函数
     *
     * @param context context
     */
    public CropImageView(Context context) {
        this(context, null);
    }

    /**
     * 构造函数
     *
     * @param context context
     * @param attrs   attrs
     */
    public CropImageView(Context context, AttrSet attrs) {
        this(context, attrs, "0");
    }

    /**
     * 构造函数
     *
     * @param context  context
     * @param attrs    attrs
     * @param defStyle defStyle
     */
    public CropImageView(Context context, AttrSet attrs, String defStyle) {
        super(context, attrs, defStyle);

        mExecutor = Executors.newSingleThreadExecutor();

        float density = getDensity();
        mHandleSize = (int) (density * HANDLE_SIZE_IN_DP);
        mMinFrameSize = density * MIN_FRAME_SIZE_IN_DP;
        mFrameStrokeWeight = density * FRAME_STROKE_WEIGHT_IN_DP;
        mGuideStrokeWeight = density * GUIDE_STROKE_WEIGHT_IN_DP;

        mPaintFrame = new Paint();
        mPaintTranslucent = new Paint();
        mPaintBitmap = new Paint();
        mPaintBitmap.setFilterBitmap(true);
        mPaintDebug = new Paint();
        mPaintDebug.setAntiAlias(true);
        mPaintDebug.setStyle(Paint.Style.STROKE_STYLE);
        mPaintDebug.setColor(new Color(WHITE));
        mPaintDebug.setTextSize(AttrHelper.fp2px(DEBUG_TEXT_SIZE_IN_DP, getContext()));

        mMatrix = new Matrix();
        mScale = 1.0f;
        mBackgroundColor = TRANSPARENT;
        mFrameColor = WHITE;
        mOverlayColor = TRANSLUCENT_BLACK;
        mHandleColor = WHITE;
        mGuideColor = TRANSLUCENT_WHITE;

        handleStyleable(context, attrs, defStyle, density);
        setEstimateSizeListener(this::onEstimateSize);
        addDrawTask(this);
        setBindStateChangedListener(this);
        setTouchEventListener(this::onTouchEvent);
    }


    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        final int viewWidth = EstimateSpec.getSize(widthMeasureSpec);
        final int viewHeight = EstimateSpec.getSize(heightMeasureSpec);
        setEstimatedSize(viewWidth, viewHeight);
        mViewWidth = viewWidth - getPaddingLeft() - getPaddingRight();
        mViewHeight = viewHeight - getPaddingTop() - getPaddingBottom();
        if (getPixelMap() != null) setupLayout(mViewWidth, mViewHeight);
        return false;
    }

    @Override
    public void postLayout() {
        super.postLayout();
        if (getPixelMap() != null) setupLayout(mViewWidth, mViewHeight);
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        canvas.drawColor(mBackgroundColor, Canvas.PorterDuffMode.CLEAR);

        if (mIsInitialized) {
            setMatrix();
            PixelMap bm = getPixelMap();


            if (bm != null) {
                canvas.drawPixelMapHolderRect(new PixelMapHolder(bm), mImageRect, mPaintBitmap);
                drawCropFrame(canvas);
            }

            if (mIsDebug) {
                drawDebugInfo(canvas);
            }
        }
    }

    @Override
    public void onComponentBoundToWindow(Component component) {
        mExecutor.shutdown();
    }

    private void handleStyleable(Context context, AttrSet attrs, String defStyle, float mDensity) {
        mCropMode = CropMode.SQUARE;
        for (CropMode mode : CropMode.values()) {
            int modeId = 3;
            if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_crop_mode).isPresent()) {
                String str = attrs.getAttr(CropImageViewAttr.scv_crop_mode).get().getStringValue();
                if ("fit_image".equals(str)) {
                    modeId = 0;
                } else if ("ratio_4_3".equals(str)) {
                    modeId = 1;
                } else if ("ratio_3_4".equals(str)) {
                    modeId = 2;
                } else if ("square".equals(str)) {
                    modeId = 3;
                } else if ("ratio_16_9".equals(str)) {
                    modeId = 4;
                } else if ("ratio_9_16".equals(str)) {
                    modeId = 5;
                } else if ("free".equals(str)) {
                    modeId = 6;
                } else if ("custom".equals(str)) {
                    modeId = 7;
                } else if ("circle".equals(str)) {
                    modeId = 8;
                } else if ("circle_square".equals(str)) {
                    modeId = 9;
                }
            }
            if (modeId == mode.getId()) {
                mCropMode = mode;
                break;
            }
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_background_color).isPresent()) {
            mBackgroundColor = attrs.getAttr(CropImageViewAttr.scv_background_color).get().getColorValue().getValue();
        } else {
            mBackgroundColor = TRANSPARENT;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_overlay_color).isPresent()) {
            mOverlayColor = attrs.getAttr(CropImageViewAttr.scv_overlay_color).get().getColorValue().getValue();
        } else {
            mOverlayColor = TRANSLUCENT_BLACK;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_frame_color).isPresent()) {
            mFrameColor = attrs.getAttr(CropImageViewAttr.scv_frame_color).get().getColorValue().getValue();
        } else {
            mFrameColor = WHITE;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_handle_color).isPresent()) {
            mHandleColor = attrs.getAttr(CropImageViewAttr.scv_handle_color).get().getColorValue().getValue();
        } else {
            mHandleColor = WHITE;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_guide_color).isPresent()) {
            mGuideColor = attrs.getAttr(CropImageViewAttr.scv_guide_color).get().getColorValue().getValue();
        } else {
            mGuideColor = TRANSLUCENT_WHITE;
        }
        for (ShowMode mode : ShowMode.values()) {
            int modeId = 1;
            if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_guide_show_mode).isPresent()) {
                String str = attrs.getAttr(CropImageViewAttr.scv_guide_show_mode).get().getStringValue();
                if ("show_always".equals(str)) {
                    modeId = 1;
                } else if ("show_on_touch".equals(str)) {
                    modeId = 2;
                } else if ("not_show".equals(str)) {
                    modeId = 3;
                }
            }
            if (modeId == mode.getId()) {
                mGuideShowMode = mode;
                break;
            }
        }

        for (ShowMode mode : ShowMode.values()) {
            int modeId = 1;
            if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_guide_show_mode).isPresent()) {
                String str = attrs.getAttr(CropImageViewAttr.scv_guide_show_mode).get().getStringValue();
                if ("show_always".equals(str)) {
                    modeId = 1;
                } else if ("show_on_touch".equals(str)) {
                    modeId = 2;
                } else if ("not_show".equals(str)) {
                    modeId = 3;
                }
            }
            if (modeId == mode.getId()) {
                mHandleShowMode = mode;
                break;
            }
        }
        setGuideShowMode(mGuideShowMode);
        setHandleShowMode(mHandleShowMode);
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_handle_size).isPresent()) {
            mHandleSize = attrs.getAttr(CropImageViewAttr.scv_handle_size).get().getDimensionValue();
        } else {
            mHandleSize = (int) (HANDLE_SIZE_IN_DP * mDensity);
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_touch_padding).isPresent()) {
            mTouchPadding = attrs.getAttr(CropImageViewAttr.scv_touch_padding).get().getDimensionValue();
        } else {
            mTouchPadding = 0;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_min_frame_size).isPresent()) {
            mMinFrameSize = attrs.getAttr(CropImageViewAttr.scv_min_frame_size).get().getDimensionValue();
        } else {
            mMinFrameSize = (int) (MIN_FRAME_SIZE_IN_DP * mDensity);
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_frame_stroke_weight).isPresent()) {
            mFrameStrokeWeight = attrs.getAttr(CropImageViewAttr.scv_frame_stroke_weight).get().getDimensionValue();
        } else {
            mFrameStrokeWeight = (int) (FRAME_STROKE_WEIGHT_IN_DP * mDensity);
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_guide_stroke_weight).isPresent()) {
            mGuideStrokeWeight = attrs.getAttr(CropImageViewAttr.scv_guide_stroke_weight).get().getDimensionValue();
        } else {
            mGuideStrokeWeight = (int) (GUIDE_STROKE_WEIGHT_IN_DP * mDensity);
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_crop_enabled).isPresent()) {
            mIsCropEnabled = attrs.getAttr(CropImageViewAttr.scv_crop_enabled).get().getBoolValue();
        } else {
            mIsCropEnabled = true;
        }
        float frameScale = DEFAULT_INITIAL_FRAME_SCALE;
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_initial_frame_scale).isPresent()) {
            frameScale = attrs.getAttr(CropImageViewAttr.scv_initial_frame_scale).get().getFloatValue();
        }
        mInitialFrameScale = constrain(frameScale, 0.01f, 1.0f, DEFAULT_INITIAL_FRAME_SCALE);
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_animation_enabled).isPresent()) {
            mIsAnimationEnabled = attrs.getAttr(CropImageViewAttr.scv_animation_enabled).get().getBoolValue();
        } else {
            mIsAnimationEnabled = true;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_animation_duration).isPresent()) {
            mAnimationDurationMillis = attrs.getAttr(CropImageViewAttr.scv_animation_duration).get().getIntegerValue();
        } else {
            mAnimationDurationMillis = DEFAULT_ANIMATION_DURATION_MILLIS;
        }
        if (attrs != null && attrs.getAttr(CropImageViewAttr.scv_handle_shadow_enabled).isPresent()) {
            mIsHandleShadowEnabled = attrs.getAttr(CropImageViewAttr.scv_handle_shadow_enabled).get().getBoolValue();
        } else {
            mIsHandleShadowEnabled = true;
        }
    }

    private void drawDebugInfo(Canvas canvas) {
        Paint.FontMetrics fontMetrics = mPaintDebug.getFontMetrics();
        mPaintDebug.measureText("W");
        int textHeight = (int) (fontMetrics.descent - fontMetrics.ascent);
        int x = (int) (mImageRect.left + (float) mHandleSize * 0.5f * getDensity());
        int y = (int) (mImageRect.top + textHeight + (float) mHandleSize * 0.5f * getDensity());
        StringBuilder builder = new StringBuilder();
        builder.append("LOADED FROM: ").append(mSourceUri != null ? "Uri" : "Bitmap");
        canvas.drawText(mPaintDebug, builder.toString(), x, y);
        builder = new StringBuilder();

        if (mSourceUri == null) {
            builder.append("INPUT_IMAGE_SIZE: ")
                    .append((int) mImgWidth)
                    .append("x")
                    .append((int) mImgHeight);
            y += textHeight;
            canvas.drawText(mPaintDebug, builder.toString(), x, y);
            builder = new StringBuilder();
        } else {
            builder = new StringBuilder().append("INPUT_IMAGE_SIZE: ")
                    .append(mInputImageWidth)
                    .append("x")
                    .append(mInputImageHeight);
            y += textHeight;
            canvas.drawText(mPaintDebug, builder.toString(), x, y);
            builder = new StringBuilder();
        }
        builder.append("LOADED_IMAGE_SIZE: ")
                .append(getPixelMap().getImageInfo().size.width)
                .append("x")
                .append(getPixelMap().getImageInfo().size.height);
        y += textHeight;
        canvas.drawText(mPaintDebug, builder.toString(), x, y);
        builder = new StringBuilder();
        if (mOutputImageWidth > 0 && mOutputImageHeight > 0) {
            builder.append("OUTPUT_IMAGE_SIZE: ")
                    .append(mOutputImageWidth)
                    .append("x")
                    .append(mOutputImageHeight);
            y += textHeight;
            canvas.drawText(mPaintDebug, builder.toString(), x, y);
            builder = new StringBuilder().append("EXIF ROTATION: ").append(mExifRotation);
            y += textHeight;
            canvas.drawText(mPaintDebug, builder.toString(), x, y);
            builder = new StringBuilder().append("CURRENT_ROTATION: ").append((int) mAngle);
            y += textHeight;
            canvas.drawText(mPaintDebug, builder.toString(), x, y);
        }
        builder = new StringBuilder();
        builder.append("FRAME_RECT: ").append(mFrameRect.toString());
        y += textHeight;
        canvas.drawText(mPaintDebug, builder.toString(), x, y);
        builder = new StringBuilder();
        builder.append("ACTUAL_CROP_RECT: ").append(getActualCropRect() != null ? getActualCropRect().toString() : "");
        y += textHeight;
        canvas.drawText(mPaintDebug, builder.toString(), x, y);
    }

    private void drawCropFrame(Canvas canvas) {
        if (!mIsCropEnabled) return;
        if (mIsRotating) return;
        drawOverlay(canvas);
        drawFrame(canvas);
        if (mShowGuide) drawGuidelines(canvas);
        if (mShowHandle) drawHandles(canvas);
    }

    private void drawOverlay(Canvas canvas) {
        mPaintTranslucent.setAntiAlias(true);
        mPaintTranslucent.setFilterBitmap(true);
        mPaintTranslucent.setColor(new Color(mOverlayColor));
        mPaintTranslucent.setStyle(Paint.Style.FILL_STYLE);
        Path path = new Path();
        RectFloat overlayRect =
                new RectFloat((float) Math.floor(mImageRect.left), (float) Math.floor(mImageRect.top),
                        (float) Math.ceil(mImageRect.right), (float) Math.ceil(mImageRect.bottom));
        if (!mIsAnimating && (mCropMode == CropMode.CIRCLE || mCropMode == CropMode.CIRCLE_SQUARE)) {
            path.addRect(overlayRect, Path.Direction.CLOCK_WISE);
            Point circleCenter = new Point((mFrameRect.left + mFrameRect.right) / 2,
                    (mFrameRect.top + mFrameRect.bottom) / 2);
            float circleRadius = (mFrameRect.right - mFrameRect.left) / 2;
            path.addCircle(circleCenter.getPointX(), circleCenter.getPointY(), circleRadius, Path.Direction.COUNTER_CLOCK_WISE);
            canvas.drawPath(path, mPaintTranslucent);
        } else {
            path.addRect(overlayRect, Path.Direction.CLOCK_WISE);
            path.addRect(mFrameRect, Path.Direction.COUNTER_CLOCK_WISE);
            canvas.drawPath(path, mPaintTranslucent);
        }
    }

    private void drawFrame(Canvas canvas) {
        mPaintFrame.setAntiAlias(true);
        mPaintFrame.setFilterBitmap(true);
        mPaintFrame.setStyle(Paint.Style.STROKE_STYLE);
        mPaintFrame.setColor(new Color(mFrameColor));
        mPaintFrame.setStrokeWidth(mFrameStrokeWeight);
        canvas.drawRect(mFrameRect, mPaintFrame);
    }

    private void drawGuidelines(Canvas canvas) {
        mPaintFrame.setColor(new Color(mGuideColor));
        mPaintFrame.setStrokeWidth(mGuideStrokeWeight);
        float h1 = mFrameRect.left + (mFrameRect.right - mFrameRect.left) / 3.0f;
        float h2 = mFrameRect.right - (mFrameRect.right - mFrameRect.left) / 3.0f;
        float v1 = mFrameRect.top + (mFrameRect.bottom - mFrameRect.top) / 3.0f;
        float v2 = mFrameRect.bottom - (mFrameRect.bottom - mFrameRect.top) / 3.0f;
        canvas.drawLine(new Point(h1, mFrameRect.top), new Point(h1, mFrameRect.bottom), mPaintFrame);
        canvas.drawLine(new Point(h2, mFrameRect.top), new Point(h2, mFrameRect.bottom), mPaintFrame);
        canvas.drawLine(new Point(mFrameRect.left, v1), new Point(mFrameRect.right, v1), mPaintFrame);
        canvas.drawLine(new Point(mFrameRect.left, v2), new Point(mFrameRect.right, v2), mPaintFrame);
    }

    private void drawHandles(Canvas canvas) {
        mPaintFrame.setStyle(Paint.Style.FILL_STYLE);
        mPaintFrame.setStrokeWidth(8f);
        mPaintFrame.setColor(new Color(mHandleColor));
        RectFloat rect = new RectFloat(mFrameRect);
        rect.left -= 3;
        rect.top -= 3;
        rect.bottom += 3;
        rect.right += 3;
        drawCorner(canvas, rect);
    }

    //画角落
    private void drawCorner(Canvas canvas, RectFloat clipBoundRect) {
        //画角落
        float cornerWidth = (clipBoundRect.right - clipBoundRect.left) / 15;

        //左上角
        canvas.drawLine(new Point(clipBoundRect.left, clipBoundRect.top),
                new Point(clipBoundRect.left + cornerWidth, clipBoundRect.top), mPaintFrame);
        canvas.drawLine(new Point(clipBoundRect.left, clipBoundRect.top - mPaintFrame.getStrokeWidth() / 2),
                new Point(clipBoundRect.left, clipBoundRect.top + cornerWidth), mPaintFrame);

        //右上角
        canvas.drawLine(new Point(clipBoundRect.right, clipBoundRect.top),
                new Point(clipBoundRect.right - cornerWidth, clipBoundRect.top), mPaintFrame);
        canvas.drawLine(new Point(clipBoundRect.right, clipBoundRect.top - mPaintFrame.getStrokeWidth() / 2),
                new Point(clipBoundRect.right, clipBoundRect.top + cornerWidth), mPaintFrame);

        //左下角
        canvas.drawLine(new Point(clipBoundRect.left, clipBoundRect.bottom),
                new Point(clipBoundRect.left + cornerWidth, clipBoundRect.bottom), mPaintFrame);
        canvas.drawLine(new Point(clipBoundRect.left, clipBoundRect.bottom + mPaintFrame.getStrokeWidth() / 2),
                new Point(clipBoundRect.left, clipBoundRect.bottom - cornerWidth), mPaintFrame);

        //右上角
        canvas.drawLine(new Point(clipBoundRect.right, clipBoundRect.bottom),
                new Point(clipBoundRect.right - cornerWidth, clipBoundRect.bottom), mPaintFrame);
        canvas.drawLine(new Point(clipBoundRect.right, clipBoundRect.bottom + mPaintFrame.getStrokeWidth() / 2),
                new Point(clipBoundRect.right, clipBoundRect.bottom - cornerWidth), mPaintFrame);
    }

    private void drawHandleShadows(Canvas canvas) {
        mPaintFrame.setStyle(Paint.Style.FILL_STYLE);
        mPaintFrame.setColor(new Color(TRANSLUCENT_BLACK));
        RectFloat rect = new RectFloat(mFrameRect);
        rect.top += 1;
        rect.bottom += 1;
        canvas.drawCircle(rect.left, rect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.right, rect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.left, rect.bottom, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.right, rect.bottom, mHandleSize, mPaintFrame);
    }

    private void setMatrix() {
        mMatrix.reset();
        mMatrix.setTranslate(mCenter.getPointX() - mImgWidth * 0.5f, mCenter.getPointY() - mImgHeight * 0.5f);
        mMatrix.postScale(mScale, mScale, mCenter.getPointX(), mCenter.getPointY());
        mMatrix.postRotate(mAngle, mCenter.getPointX(), mCenter.getPointY());
    }

    private void setupLayout(int viewW, int viewH) {
        if (viewW == 0 || viewH == 0) return;
        viewW = viewW + getPaddingLeft() + getPaddingRight();
        viewH = viewH + getPaddingTop() + getPaddingBottom();
        setCenter(new Point(getPaddingLeft() + viewW * 0.5f, getPaddingTop() + viewH * 0.5f));
        setScale(calcScale(viewW, viewH, mAngle));
        setMatrix();
        mImageRect = calcImageRect(new RectFloat(0f, 0f, mImgWidth, mImgHeight), mMatrix);

        if (mInitialFrameRect != null) {
            mFrameRect = applyInitialFrameRect(mInitialFrameRect);
        } else {
            mFrameRect = calcFrameRect(mImageRect);
        }
        mIsInitialized = true;
        invalidate();
    }

    private float calcScale(int viewW, int viewH, float angle) {
        mImgWidth = getPixelMap().getImageInfo().size.width;
        mImgHeight = getPixelMap().getImageInfo().size.height;
        if (mImgWidth <= 0) mImgWidth = viewW;
        if (mImgHeight <= 0) mImgHeight = viewH;
        float viewRatio = (float) viewW / (float) viewH;
        float imgRatio = getRotatedWidth(angle) / getRotatedHeight(angle);
        float scale = 1.0f;
        if (imgRatio >= viewRatio) {
            scale = viewW / getRotatedWidth(angle);
        } else if (imgRatio < viewRatio) {
            scale = viewH / getRotatedHeight(angle);
        }
        return scale;
    }

    private RectFloat calcImageRect(RectFloat rect, Matrix matrix) {
        RectFloat applied = new RectFloat();
        matrix.mapRect(applied, rect);
        return applied;
    }

    private RectFloat calcFrameRect(RectFloat imageRect) {
        float frameW = getRatioX(imageRect.getWidth());
        float frameH = getRatioY(imageRect.getHeight());
        float imgRatio = imageRect.getWidth() / imageRect.getHeight();
        float frameRatio = frameW / frameH;
        float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
        if (frameRatio >= imgRatio) {
            l = imageRect.left;
            r = imageRect.right;
            float hy = (imageRect.top + imageRect.bottom) * 0.5f;
            float hh = (imageRect.getWidth() / frameRatio) * 0.5f;
            t = hy - hh;
            b = hy + hh;
        } else if (frameRatio < imgRatio) {
            t = imageRect.top;
            b = imageRect.bottom;
            float hx = (imageRect.left + imageRect.right) * 0.5f;
            float hw = imageRect.getHeight() * frameRatio * 0.5f;
            l = hx - hw;
            r = hx + hw;
        }
        float w = r - l;
        float h = b - t;
        float cx = l + w / 2;
        float cy = t + h / 2;
        float sw = w * mInitialFrameScale;
        float sh = h * mInitialFrameScale;
        return new RectFloat(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (!mIsInitialized) return false;
        if (!mIsCropEnabled) return false;
        if (!mIsEnabled) return false;
        if (mIsRotating) return false;
        if (mIsAnimating) return false;
        if (mIsLoading.get()) return false;
        if (mIsCropping.get()) return false;

        switch (event.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                onDown(event);
                return true;
            case TouchEvent.POINT_MOVE:
                onMove(event);
                if (mTouchArea != TouchArea.OUT_OF_BOUNDS) {

                }
                return true;
            case TouchEvent.CANCEL:
                onCancel();
                return true;
            case TouchEvent.PRIMARY_POINT_UP:
                onUp(event);
                if (isMove) {
                    try {
                        Thread.sleep(1200);
                        resetRect();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return true;
        }
        return false;
    }

    private void resetRect() {
        float left = mFrameRect.left;
        float top = mFrameRect.top;
        float right = mFrameRect.right + mFrameRect.getWidth() * 0.15f;
        float bottom = mFrameRect.bottom + mFrameRect.getHeight() * 0.15f;

        float left_img = mImageRect.left;
        float top_img = mImageRect.top;
        float right_img = mImageRect.right + mImageRect.getWidth() * 0.15f;
        float bottom_img = mImageRect.bottom + mImageRect.getHeight() * 0.15f;

        mImageRect = new RectFloat(left_img, top_img, right_img, bottom_img);
        mFrameRect = new RectFloat(left, top, right, bottom);

        float mCenterX = mFrameRect.getCenter().getPointX();
        float mCenterY = mFrameRect.getCenter().getPointY();
        mFrameRect.translateCenterTo(mCenter.getPointX(), mCenter.getPointY());
        mImageRect.translateTo(mImageRect.left + mCenter.getPointX() - mCenterX,
                mImageRect.top + mCenter.getPointY() - mCenterY);
        isMove = false;
    }

    private void onDown(TouchEvent e) {
        invalidate();
        MmiPoint mmiPoint = e.getPointerPosition(0);
        mLastX = mmiPoint.getX();
        mLastY = mmiPoint.getY();
        checkTouchArea(mmiPoint.getX(), mmiPoint.getY());
    }

    private void onMove(TouchEvent e) {
        MmiPoint mmiPoint = e.getPointerPosition(0);
        float diffX = mmiPoint.getX() - mLastX;
        float diffY = mmiPoint.getY() - mLastY;
        switch (mTouchArea) {
            case CENTER:
                isMove = true;
                moveFrame(diffX, diffY);
                break;
            case LEFT_TOP:
                isMove = true;
                moveHandleLT(diffX, diffY);
                break;
            case RIGHT_TOP:
                isMove = true;
                moveHandleRT(diffX, diffY);
                break;
            case LEFT_BOTTOM:
                isMove = true;
                moveHandleLB(diffX, diffY);
                break;
            case RIGHT_BOTTOM:
                isMove = true;
                moveHandleRB(diffX, diffY);
                break;
            case LEFT:
                isMove = true;
                moveFrameLeft(diffX, diffY);
                break;
            case TOP:
                isMove = true;
                moveFrameTop(diffX, diffY);
                break;
            case RIGHT:
                isMove = true;
                moveFrameRight(diffX, diffY);
                break;
            case BOTTOM:
                isMove = true;
                moveFrameBottom(diffX, diffY);
                break;
            case OUT_OF_BOUNDS:
                isMove = false;
                break;
        }
        invalidate();
        mLastX = mmiPoint.getX();
        mLastY = mmiPoint.getY();
    }

    private void onUp(TouchEvent e) {
        if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = false;
        if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = false;
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
        invalidate();
    }

    private void onCancel() {
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
        invalidate();
    }

    private void checkTouchArea(float x, float y) {
        if (isInsideCornerLeftTop(x, y)) {
            mTouchArea = TouchArea.LEFT_TOP;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerRightTop(x, y)) {
            mTouchArea = TouchArea.RIGHT_TOP;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerLeftBottom(x, y)) {
            mTouchArea = TouchArea.LEFT_BOTTOM;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerRightBottom(x, y)) {
            mTouchArea = TouchArea.RIGHT_BOTTOM;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideFrame(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.CENTER;
            return;
        }
        if (isInsideFrameLeft(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.LEFT;
            return;
        }
        if (isInsideFrameTop(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.TOP;
            return;
        }
        if (isInsideFrameRight(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.RIGHT;
            return;
        }
        if (isInsideFrameBottom(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.BOTTOM;
            return;
        }
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
    }

    private boolean isInsideFrame(float x, float y) {
        if (mFrameRect.left + moreLength < x && mFrameRect.right - moreLength > x) {
            if (mFrameRect.top < y && mFrameRect.bottom > y) {
                mTouchArea = TouchArea.CENTER;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideFrameLeft(float x, float y) {
        if (mFrameRect.left - moreLength < x && x < mFrameRect.left + moreLength) {
            if (mFrameRect.top <= y && mFrameRect.bottom >= y) {
                mTouchArea = TouchArea.LEFT;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideFrameTop(float x, float y) {
        if (mFrameRect.top - moreLength < y && y < mFrameRect.top + moreLength) {
            if (mFrameRect.left <= x && mFrameRect.right >= x) {
                mTouchArea = TouchArea.TOP;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideFrameRight(float x, float y) {
        if (mFrameRect.right - moreLength < x && x < mFrameRect.right + moreLength) {
            if (mFrameRect.top <= y && mFrameRect.bottom >= y) {
                mTouchArea = TouchArea.RIGHT;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideFrameBottom(float x, float y) {
        if (mFrameRect.bottom - moreLength < y && y < mFrameRect.bottom + moreLength) {
            if (mFrameRect.left <= x && mFrameRect.right >= x) {
                mTouchArea = TouchArea.BOTTOM;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideCornerLeftTop(float x, float y) {
        float dx = x - mFrameRect.left;
        float dy = y - mFrameRect.top;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerRightTop(float x, float y) {
        float dx = x - mFrameRect.right;
        float dy = y - mFrameRect.top;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerLeftBottom(float x, float y) {
        float dx = x - mFrameRect.left;
        float dy = y - mFrameRect.bottom;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerRightBottom(float x, float y) {
        float dx = x - mFrameRect.right;
        float dy = y - mFrameRect.bottom;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private void moveFrame(float x, float y) {
        mFrameRect.left += x;
        mFrameRect.right += x;
        mFrameRect.top += y;
        mFrameRect.bottom += y;
        checkMoveBounds();
    }

    private void moveFrameLeft(float x, float y) {
        mFrameRect.left += x;
        checkMoveBounds();
    }

    private void moveFrameTop(float x, float y) {
        mFrameRect.top += y;
        checkMoveBounds();
    }

    private void moveFrameRight(float x, float y) {
        mFrameRect.right += x;
        checkMoveBounds();
    }

    private void moveFrameBottom(float x, float y) {
        mFrameRect.bottom += y;
        checkMoveBounds();
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleLT(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.left += diffX;
            mFrameRect.top += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.left += dx;
            mFrameRect.top += dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.left -= offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.left)) {
                ox = mImageRect.left - mFrameRect.left;
                mFrameRect.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.top += oy;
            }
            if (!isInsideVertical(mFrameRect.top)) {
                oy = mImageRect.top - mFrameRect.top;
                mFrameRect.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.left += ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleRT(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.right += diffX;
            mFrameRect.top += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.right += dx;
            mFrameRect.top -= dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.right += offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.right)) {
                ox = mFrameRect.right - mImageRect.right;
                mFrameRect.right -= ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.top += oy;
            }
            if (!isInsideVertical(mFrameRect.top)) {
                oy = mImageRect.top - mFrameRect.top;
                mFrameRect.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.right -= ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleLB(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.left += diffX;
            mFrameRect.bottom += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.left += dx;
            mFrameRect.bottom -= dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.bottom += offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.left -= offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.left)) {
                ox = mImageRect.left - mFrameRect.left;
                mFrameRect.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.bottom -= oy;
            }
            if (!isInsideVertical(mFrameRect.bottom)) {
                oy = mFrameRect.bottom - mImageRect.bottom;
                mFrameRect.bottom -= oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.left += ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleRB(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.right += diffX;
            mFrameRect.bottom += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right +=
                        offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.right += dx;
            mFrameRect.bottom += dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.bottom += offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.right += offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.right)) {
                ox = mFrameRect.right - mImageRect.right;
                mFrameRect.right -= ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.bottom -= oy;
            }
            if (!isInsideVertical(mFrameRect.bottom)) {
                oy = mFrameRect.bottom - mImageRect.bottom;
                mFrameRect.bottom -= oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.right -= ox;
            }
        }
    }

    private void checkScaleBounds() {
        float lDiff = mFrameRect.left - mImageRect.left;
        float rDiff = mFrameRect.right - mImageRect.right;
        float tDiff = mFrameRect.top - mImageRect.top;
        float bDiff = mFrameRect.bottom - mImageRect.bottom;

        if (lDiff < 0) {
            mFrameRect.left -= lDiff;
        }
        if (rDiff > 0) {
            mFrameRect.right -= rDiff;
        }
        if (tDiff < 0) {
            mFrameRect.top -= tDiff;
        }
        if (bDiff > 0) {
            mFrameRect.bottom -= bDiff;
        }
    }

    private void checkMoveBounds() {
        float diff = mFrameRect.left - mImageRect.left;
        if (diff < 0) {
            mFrameRect.left -= diff;
            mFrameRect.right -= diff;
        }
        diff = mFrameRect.right - mImageRect.right;
        if (diff > 0) {
            mFrameRect.left -= diff;
            mFrameRect.right -= diff;
        }
        diff = mFrameRect.top - mImageRect.top;
        if (diff < 0) {
            mFrameRect.top -= diff;
            mFrameRect.bottom -= diff;
        }
        diff = mFrameRect.bottom - mImageRect.bottom;
        if (diff > 0) {
            mFrameRect.top -= diff;
            mFrameRect.bottom -= diff;
        }
    }

    private boolean isInsideHorizontal(float x) {
        return mImageRect.left <= x && mImageRect.right >= x;
    }

    private boolean isInsideVertical(float y) {
        return mImageRect.top <= y && mImageRect.bottom >= y;
    }

    private boolean isWidthTooSmall() {
        return getFrameW() < mMinFrameSize;
    }

    private boolean isHeightTooSmall() {
        return getFrameH() < mMinFrameSize;
    }

    private void recalculateFrameRect(int durationMillis) {
        if (mImageRect == null) return;
        if (mIsAnimating) {
            getAnimator().cancelAnimation();
        }
        final RectFloat currentRect = new RectFloat(mFrameRect);
        final RectFloat newRect = calcFrameRect(mImageRect);
        final float diffL = newRect.left - currentRect.left;
        final float diffT = newRect.top - currentRect.top;
        final float diffR = newRect.right - currentRect.right;
        final float diffB = newRect.bottom - currentRect.bottom;
        if (mIsAnimationEnabled) {
            SimpleValueAnimator animator = getAnimator();
            animator.addAnimatorListener(new SimpleValueAnimatorListener() {
                @Override
                public void onAnimationStarted() {
                    mIsAnimating = true;
                }

                @Override
                public void onAnimationUpdated(float scale) {
                    mFrameRect = new RectFloat(currentRect.left + diffL * scale, currentRect.top + diffT * scale,
                            currentRect.right + diffR * scale, currentRect.bottom + diffB * scale);
                    invalidate();
                }

                @Override
                public void onAnimationFinished() {
                    mFrameRect = newRect;
                    invalidate();
                    mIsAnimating = false;
                }
            });
            animator.startAnimation(durationMillis);
        } else {
            mFrameRect = calcFrameRect(mImageRect);
            invalidate();
        }
    }

    private float getRatioX(float w) {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.getWidth();
            case FREE:
                return w;
            case RATIO_4_3:
                return 4;
            case RATIO_3_4:
                return 3;
            case RATIO_16_9:
                return 16;
            case RATIO_9_16:
                return 9;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.getPointX();
            default:
                return w;
        }
    }

    private float getRatioY(float h) {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.getHeight();
            case FREE:
                return h;
            case RATIO_4_3:
                return 3;
            case RATIO_3_4:
                return 4;
            case RATIO_16_9:
                return 9;
            case RATIO_9_16:
                return 16;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.getPointY();
            default:
                return h;
        }
    }

    private float getRatioX() {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.getWidth();
            case RATIO_4_3:
                return 4;
            case RATIO_3_4:
                return 3;
            case RATIO_16_9:
                return 16;
            case RATIO_9_16:
                return 9;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.getPointX();
            default:
                return 1;
        }
    }

    private float getRatioY() {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.getHeight();
            case RATIO_4_3:
                return 3;
            case RATIO_3_4:
                return 4;
            case RATIO_16_9:
                return 9;
            case RATIO_9_16:
                return 16;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.getPointY();

            default:
                return 1;
        }
    }

    private float getDensity() {
        return AttrHelper.getDensity(getContext());
    }

    private float sq(float value) {
        return value * value;
    }

    private float constrain(float val, float min, float max, float defaultVal) {
        if (val < min || val > max) return defaultVal;
        return val;


    }

    private void postErrorOnMainThread(final Callback callback, final Throwable e) {
        if (callback == null) return;
        if (EventRunner.current() == EventRunner.getMainEventRunner()) {
            callback.onError(e);
        } else {
            mHandler.postTask(new Runnable() {
                @Override
                public void run() {
                    callback.onError(e);
                }
            });
        }
    }

    private float getRotatedWidth(float angle) {
        return getRotatedWidth(angle, mImgWidth, mImgHeight);
    }

    private float getRotatedWidth(float angle, float width, float height) {
        return angle % 180 == 0 ? width : height;
    }

    private float getRotatedHeight(float angle) {
        return getRotatedHeight(angle, mImgWidth, mImgHeight);
    }

    private float getRotatedHeight(float angle, float width, float height) {
        return angle % 180 == 0 ? height : width;
    }

    private PixelMap getRotatedBitmap(PixelMap bitmap) {
        Matrix rotateMatrix = new Matrix();
        rotateMatrix.setRotate(mAngle, bitmap.getImageInfo().size.width / 2, bitmap.getImageInfo().size.height / 2);
        PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
        initializationOptions.size = new Size(bitmap.getImageInfo().size.width, bitmap.getImageInfo().size.height);
        return PixelMap.create(bitmap, initializationOptions);
    }

    public RectFloat getFrameRect() {
        return mFrameRect;
    }

    private SimpleValueAnimator getAnimator() {
        setupAnimatorIfNeeded();
        return mAnimator;
    }

    private void setupAnimatorIfNeeded() {
        if (mAnimator == null) {
            mAnimator = new ValueAnimatorV14(mInterpolator);
        }
    }

    private PixelMap getCroppedBitmapFromUri() throws IOException {
        PixelMap cropped = null;
        DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
        try {
            FileDescriptor fd = helper.openFile(mSourceUri, "r");
            ImageSource imageSource = ImageSource.create(fd, null);
            Rect cropRect = calcCropRect(imageSource.getImageInfo().size.width, imageSource.getImageInfo().size.height);
            if (mAngle != 0) {
                Matrix matrix = new Matrix();
                matrix.setRotate(-mAngle);
                RectFloat rotated = new RectFloat();
                matrix.mapRect(rotated, new RectFloat(cropRect));
                int height = rotated.top < 0 ? imageSource.getImageInfo().size.height : 0;
                int width = rotated.left < 0 ? imageSource.getImageInfo().size.width : 0;
                rotated.left += width;
                rotated.right += width;
                rotated.top += height;
                rotated.bottom += height;
                cropRect = new Rect((int) rotated.left, (int) rotated.top, (int) rotated.right,
                        (int) rotated.bottom);
            }
            ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
            decodingOptions.desiredRegion = new ohos.media.image.common.Rect(cropRect.left, cropRect.top, cropRect.right - cropRect.left, cropRect.bottom - cropRect.top);
            decodingOptions.rotateDegrees = rotateDegrees;
            cropped = imageSource.createPixelmap(decodingOptions);
            if (mAngle != 0) {
                PixelMap rotated = getRotatedBitmap(cropped);

                if (cropped != getPixelMap() && cropped != rotated) {
                    cropped.release();
                }
                cropped = rotated;
            }
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
        return cropped;
    }

    private Rect calcCropRect(int originalImageWidth, int originalImageHeight) {
        mAngle = rotateDegrees % 360;
        float scaleToOriginal =
                getRotatedWidth(mAngle, originalImageWidth, originalImageHeight) / mImageRect.getWidth();
        float offsetX = mImageRect.left * scaleToOriginal;
        float offsetY = mImageRect.top * scaleToOriginal;
        int left = Math.round(mFrameRect.left * scaleToOriginal - offsetX);
        int top = Math.round(mFrameRect.top * scaleToOriginal - offsetY);
        int right = Math.round(mFrameRect.right * scaleToOriginal - offsetX);
        int bottom = Math.round(mFrameRect.bottom * scaleToOriginal - offsetY);
        int imageW = Math.round(getRotatedWidth(mAngle, originalImageWidth, originalImageHeight));
        int imageH = Math.round(getRotatedHeight(mAngle, originalImageWidth, originalImageHeight));
        return new Rect(Math.max(left, 0), Math.max(top, 0), Math.min(right, imageW),
                Math.min(bottom, imageH));
    }

    private PixelMap scaleBitmapIfNeeded(PixelMap cropped) {
        int width = cropped.getImageInfo().size.width;
        int height = cropped.getImageInfo().size.height;
        int outWidth = 0;
        int outHeight = 0;
        float imageRatio = getRatioX(mFrameRect.getWidth()) / getRatioY(mFrameRect.getHeight());

        if (mOutputWidth > 0) {
            outWidth = mOutputWidth;
            outHeight = Math.round(mOutputWidth / imageRatio);
        } else if (mOutputHeight > 0) {
            outHeight = mOutputHeight;
            outWidth = Math.round(mOutputHeight * imageRatio);
        } else {
            if (mOutputMaxWidth > 0 && mOutputMaxHeight > 0 && (width > mOutputMaxWidth
                    || height > mOutputMaxHeight)) {
                float maxRatio = (float) mOutputMaxWidth / (float) mOutputMaxHeight;
                if (maxRatio >= imageRatio) {
                    outHeight = mOutputMaxHeight;
                    outWidth = Math.round((float) mOutputMaxHeight * imageRatio);
                } else {
                    outWidth = mOutputMaxWidth;
                    outHeight = Math.round((float) mOutputMaxWidth / imageRatio);
                }
            }
        }

        if (outWidth > 0 && outHeight > 0) {
            PixelMap scaled = Utils.getScaledBitmap(cropped, outWidth, outHeight);
            if (cropped != getPixelMap() && cropped != scaled) {
                cropped.release();
            }
            cropped = scaled;
        }
        return cropped;
    }

    /**
     * saveImage
     *
     * @param fileName fileName
     * @param pixelMap pixelMap
     * @return Uri
     */
    public Uri saveImage(String fileName, PixelMap pixelMap) {
        try {
            ValuesBucket valuesBucket = new ValuesBucket();
            valuesBucket.putString(AVStorage.Images.Media.DISPLAY_NAME, fileName);
            valuesBucket.putString("relative_path", "DCIM/");
            valuesBucket.putString(AVStorage.Images.Media.MIME_TYPE, "image/" + mCompressFormat);
            valuesBucket.putInteger("is_pending", 1);
            DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
            int id = helper.insert(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, valuesBucket);
            Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, String.valueOf(id));
            FileDescriptor fd = helper.openFile(uri, "w");
            ImagePacker imagePacker = ImagePacker.create();
            ImagePacker.PackingOptions packingOptions = new ImagePacker.PackingOptions();
            OutputStream outputStream = new FileOutputStream(fd);
            packingOptions.quality = 90;
            boolean result = imagePacker.initializePacking(outputStream, packingOptions);
            if (result) {
                result = imagePacker.addImage(pixelMap);
                if (result) {
                    long dataSize = imagePacker.finalizePacking();
                }
            }
            outputStream.flush();
            outputStream.close();
            valuesBucket.clear();
            valuesBucket.putInteger("is_pending", 0);
            helper.update(uri, valuesBucket, null);
            return uri;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * deleteImage
     *
     * @param uri uri
     */
    public void deleteImage(Uri uri) {
        try {
            DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
            helper.delete(uri, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Get source image bitmap
     *
     * @return src bitmap
     */
    public PixelMap getImagePixelMap() {
        return getPixelMap();
    }

    @Override
    public void setImageElement(Element element) {
        mIsInitialized = false;
        resetImageInfo();
        super.setImageElement(element);
        updateLayout();
    }

    private void setImageDrawableInternal(PixelMap pixelMap) {
        mIsInitialized = false;
        resetImageInfo();
        super.setPixelMap(pixelMap);
        updateLayout();
    }

    @Override
    public void setPixelMap(PixelMap pixelMap) {
        mIsInitialized = false;
        resetImageInfo();
        super.setPixelMap(pixelMap);
        updateLayout();
    }

    private void updateLayout() {
        PixelMap d = getPixelMap();
        if (d != null) {
            setupLayout(mViewWidth, mViewHeight);
        }
    }

    private void resetImageInfo() {
        if (mIsLoading.get()) return;
        mSaveUri = null;
        mInputImageWidth = 0;
        mInputImageHeight = 0;
        mOutputImageWidth = 0;
        mOutputImageHeight = 0;
        mAngle = mExifRotation;
    }

    /**
     * 还原
     */
    public void resetCrop() {
        rotateDegrees = 0;
        rotateImage(RotateDegrees.ROTATE_00D);
        postLayout();
    }

    /**
     * Load image from Uri.
     * This method is deprecated. Use loadAsync(Uri, LoadCallback) instead.
     *
     * @param sourceUri Image Uri
     * @param callback  Callback
     */
    public void startLoad(final Uri sourceUri, final LoadCallback callback) {
        loadAsync(sourceUri, callback);
    }

    /**
     * Load image from Uri.
     *
     * @param sourceUri Image Uri
     * @param callback  Callback
     */
    public void loadAsync(final Uri sourceUri, final LoadCallback callback) {
        loadAsync(sourceUri, false, null, callback);
    }

    /**
     * Load image from Uri.
     *
     * @param sourceUri Uri
     * @param useThumbnail boolean
     * @param initialFrameRect RectFloat
     * @param callback LoadCallback
     */
    public void loadAsync(final Uri sourceUri, final boolean useThumbnail,
                          final RectFloat initialFrameRect, final LoadCallback callback) {
        mExecutor = Executors.newSingleThreadExecutor();
        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mIsLoading.set(true);

                    mSourceUri = sourceUri;
                    mInitialFrameRect = initialFrameRect;

                    if (useThumbnail) {
                        applyThumbnail(sourceUri);
                    }

                    final PixelMap sampled = getImage(sourceUri);

                    mHandler.postTask(new Runnable() {
                        @Override
                        public void run() {
                            mAngle = mExifRotation;
                            setImageDrawableInternal(sampled);
                            if (callback != null) callback.onSuccess();
                        }
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(callback, e);
                } finally {
                    mIsLoading.set(false);
                }
            }
        });
    }

    /**
     * Load image from Uri with RxJava2
     *
     * @param sourceUri Uri
     * @return Completable
     */
    public Completable loadAsCompletable(final Uri sourceUri) {
        return loadAsCompletable(sourceUri, false, null);
    }

    /**
     * Load image from Uri with RxJava2
     *
     * @param sourceUri Uri
     * @param useThumbnail boolean
     * @param initialFrameRect RectFloat
     * @return Completable
     */
    public Completable loadAsCompletable(final Uri sourceUri, final boolean useThumbnail,
                                         final RectFloat initialFrameRect) {
        return Completable.create(new CompletableOnSubscribe() {

            @Override
            public void subscribe(final CompletableEmitter emitter) throws Exception {

                mInitialFrameRect = initialFrameRect;
                mSourceUri = sourceUri;

                if (useThumbnail) {
                    applyThumbnail(sourceUri);
                }

                final PixelMap sampled = getImage(sourceUri);

                mHandler.postTask(new Runnable() {
                    @Override
                    public void run() {
                        mAngle = mExifRotation;
                        setImageDrawableInternal(sampled);
                        emitter.onComplete();
                    }
                });
            }
        }).doOnSubscribe(new io.reactivex.functions.Consumer<Disposable>() {
            @Override
            public void accept(Disposable disposable) throws Exception {
                mIsLoading.set(true);
            }
        }).doFinally(new Action() {
            @Override
            public void run() throws Exception {
                mIsLoading.set(false);
            }
        });
    }

    /**
     * Load image from Uri with Builder Pattern
     *
     * @param sourceUri Image Uri
     * @return Builder
     */
    public LoadRequest load(Uri sourceUri) {
        return new LoadRequest(this, sourceUri);
    }

    private void applyThumbnail(Uri sourceUri) {
        final PixelMap thumb = getThumbnail(sourceUri);
        if (thumb == null) return;
        mHandler.postTask(new Runnable() {
            @Override
            public void run() {
                mAngle = mExifRotation;
                setImageDrawableInternal(thumb);
            }
        });
    }

    private PixelMap getImage(Uri sourceUri) {

        if (sourceUri == null) {
            throw new IllegalStateException("Source Uri must not be null.");
        }

        mExifRotation = Utils.getExifOrientation(getContext(), mSourceUri);
        int maxSize = Utils.getMaxSize();
        int requestSize = Math.max(mViewWidth, mViewHeight);
        if (requestSize == 0) requestSize = maxSize;

        final PixelMap sampledBitmap =
                Utils.decodeSampledBitmapFromUri(getContext(), mSourceUri, requestSize);
        mInputImageWidth = Utils.sInputImageWidth;
        mInputImageHeight = Utils.sInputImageHeight;
        return sampledBitmap;
    }

    private PixelMap getThumbnail(Uri sourceUri) {

        if (sourceUri == null) {
            throw new IllegalStateException("Source Uri must not be null.");
        }
        mExifRotation = Utils.getExifOrientation(getContext(), mSourceUri);
        int requestSize = (int) (Math.max(mViewWidth, mViewHeight) * 0.1f);
        if (requestSize == 0) return null;

        final PixelMap sampledBitmap =
                Utils.decodeSampledBitmapFromUri(getContext(), mSourceUri, requestSize);
        mInputImageWidth = Utils.sInputImageWidth;
        mInputImageHeight = Utils.sInputImageHeight;
        return sampledBitmap;
    }

    /**
     * Rotate image
     *
     * @param degrees        rotation angle
     * @param durationMillis animation duration in milliseconds
     */
    public void rotateImage(RotateDegrees degrees, int durationMillis) {
        if (mIsRotating) {
            getAnimator().cancelAnimation();
        }

        final float currentAngle = mAngle;
        final float newAngle = (mAngle + degrees.getValue());
        rotateDegrees += degrees.getValue();
        final float angleDiff = newAngle - currentAngle;
        final float currentScale = mScale;
        final float newScale = calcScale(mViewWidth, mViewHeight, newAngle);

        if (mIsAnimationEnabled) {
            final float scaleDiff = newScale - currentScale;
            SimpleValueAnimator animator = getAnimator();
            animator.addAnimatorListener(new SimpleValueAnimatorListener() {
                @Override
                public void onAnimationStarted() {
                    mIsRotating = true;
                }

                @Override
                public void onAnimationUpdated(float scale) {
                    mAngle = currentAngle + angleDiff * scale;
                    mScale = currentScale + scaleDiff * scale;
                    setMatrix();
                    invalidate();
                }

                @Override
                public void onAnimationFinished() {
                    mAngle = newAngle % 360;
                    mScale = newScale;
                    mInitialFrameRect = null;
                    setupLayout(mViewWidth, mViewHeight);
                    DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
                    try {
                        FileDescriptor fd = helper.openFile(mSourceUri, "r");
                        ImageSource imageSource = ImageSource.create(fd, null);
                        ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
                        decodingOptions.rotateDegrees = rotateDegrees;
                        setPixelMap(imageSource.createPixelmap(decodingOptions));
                    } catch (DataAbilityRemoteException e) {
                        e.printStackTrace();
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                    mIsRotating = false;
                }
            });
            animator.startAnimation(durationMillis);
        } else {
            mAngle = newAngle % 360;
            mScale = newScale;
            setupLayout(mViewWidth, mViewHeight);
        }
    }

    /**
     * Rotate image
     *
     * @param degrees rotation angle
     */
    public void rotateImage(RotateDegrees degrees) {
        rotateImage(degrees, mAnimationDurationMillis);
    }

    public int getRotateDegrees() {
        return rotateDegrees;
    }

    /**
     * Get cropped image bitmap
     *
     * @return cropped image bitmap
     */
    public PixelMap getCroppedBitmap() {
        PixelMap source = getPixelMap();
        if (source == null) return null;

        PixelMap rotated = getRotatedBitmap(source);
        Rect cropRect = calcCropRect(source.getImageInfo().size.width, source.getImageInfo().size.height);
        PixelMap cropped = PixelMap.create(rotated, new ohos.media.image.common.Rect(cropRect.left, cropRect.top, cropRect.getWidth(),
                cropRect.getHeight()), null);
        if (rotated != cropped && rotated != source) {
            rotated.release();
        }

        if (mCropMode == CropMode.CIRCLE) {
            PixelMap circle = getCircularBitmap(cropped);
            if (cropped != getPixelMap()) {
                cropped.release();
            }
            cropped = circle;
        }
        return cropped;
    }

    /**
     * Crop the square image in a circular
     *
     * @param square image bitmap
     * @return circular image bitmap
     */
    public PixelMap getCircularBitmap(PixelMap square) {
        if (square == null) return null;
        PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
        initializationOptions.size = new Size(square.getImageInfo().size.width, square.getImageInfo().size.height);
        initializationOptions.pixelFormat = PixelFormat.ARGB_8888;
        PixelMap output = PixelMap.create(initializationOptions);
        final RectFloat rect = new RectFloat(0, 0, square.getImageInfo().size.width, square.getImageInfo().size.height);
        Canvas canvas = new Canvas(new Texture(output));

        int halfWidth = square.getImageInfo().size.width / 2;
        int halfHeight = square.getImageInfo().size.height / 2;

        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);

        canvas.drawCircle(halfWidth, halfHeight, Math.min(halfWidth, halfHeight), paint);
        paint.setBlendMode(BlendMode.SRC_IN);
        canvas.drawPixelMapHolderRect(new PixelMapHolder(square), rect, rect, paint);
        return output;
    }

    /**
     * Crop image
     * This method is separated to #crop(Uri) and #save(Bitmap)
     * Use #crop(Uri) and #save(Bitmap)
     *
     * @param saveUri      Uri for saving the cropped image
     * @param cropCallback Callback for cropping the image
     * @param saveCallback Callback for saving the image
     * @see #crop(Uri)
     */
    public void startCrop(final Uri saveUri, final CropCallback cropCallback,
                          final SaveCallback saveCallback) {
        mExecutor = Executors.newSingleThreadExecutor();
        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                PixelMap croppedImage = null;

                try {
                    mIsCropping.set(true);

                    croppedImage = cropImage();

                    final PixelMap cropped = croppedImage;
                    mHandler.postTask(new Runnable() {
                        @Override
                        public void run() {
                            if (cropCallback != null) cropCallback.onSuccess(cropped);
                            if (mIsDebug) invalidate();
                        }
                    });

                    saveImage(System.currentTimeMillis() + "", croppedImage);

                    mHandler.postTask(new Runnable() {
                        @Override
                        public void run() {
                            if (saveCallback != null) saveCallback.onSuccess(saveUri);
                        }
                    });
                } catch (Exception e) {
                    if (croppedImage == null) {
                        postErrorOnMainThread(cropCallback, e);
                    } else {
                        postErrorOnMainThread(saveCallback, e);
                    }
                } finally {
                    mIsCropping.set(false);
                }
            }
        });
    }

    /**
     * Crop image
     *
     * @param sourceUri    Uri for cropping(If null, the Uri set in loadAsync() is used)
     * @param cropCallback Callback for cropping the image
     * @see #crop(Uri)
     */
    public void cropAsync(final Uri sourceUri, final CropCallback cropCallback) {
        mExecutor = Executors.newSingleThreadExecutor();
        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mIsCropping.set(true);

                    if (sourceUri != null) mSourceUri = sourceUri;

                    final PixelMap cropped = cropImage();

                    mHandler.postTask(new Runnable() {
                        @Override
                        public void run() {
                            if (cropCallback != null) cropCallback.onSuccess(cropped);
                            if (mIsDebug) invalidate();
                        }
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(cropCallback, e);
                } finally {
                    mIsCropping.set(false);
                }
            }
        });
    }

    /**
     * cropAsync
     *
     * @param cropCallback cropCallback
     */
    public void cropAsync(final CropCallback cropCallback) {
        cropAsync(null, cropCallback);
    }

    /**
     * Crop image with RxJava2
     *
     * @param sourceUri Uri for cropping(If null, the Uri set in loadAsSingle() is used)
     * @return Single of cropping image
     */
    public Single<PixelMap> cropAsSingle(final Uri sourceUri) {
        return Single.fromCallable(new Callable<PixelMap>() {

            @Override
            public PixelMap call() throws Exception {
                if (sourceUri != null) mSourceUri = sourceUri;
                return cropImage();
            }
        }).doOnSubscribe(new Consumer<Disposable>() {
            @Override
            public void accept(@NonNull Disposable disposable) throws Exception {
                mIsCropping.set(true);
            }
        }).doFinally(new Action() {
            @Override
            public void run() throws Exception {
                mIsCropping.set(false);
            }
        });
    }

    /**
     * cropAsSingle
     *
     * @return PixelMap
     */
    public Single<PixelMap> cropAsSingle() {
        return cropAsSingle(null);
    }

    /**
     * Crop image with Builder Pattern
     *
     * @param sourceUri Uri for cropping(If null, the Uri set in loadAsSingle() is used)
     * @return Builder
     */
    public CropRequest crop(Uri sourceUri) {
        return new CropRequest(this, sourceUri);
    }

    /**
     * Save image
     *
     * @param image        Image for saving
     * @param saveCallback Callback for saving the image
     */
    public void saveAsync(final PixelMap image, final SaveCallback saveCallback) {
        mExecutor = Executors.newSingleThreadExecutor();
        mExecutor.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    mIsSaving.set(true);
                    Uri saveUri = saveImage(System.currentTimeMillis() + "", image);

                    mHandler.postTask(new Runnable() {
                        @Override
                        public void run() {
                            if (saveCallback != null) saveCallback.onSuccess(saveUri);
                        }
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(saveCallback, e);
                } finally {
                    mIsSaving.set(false);
                }
            }
        });
    }

    /**
     * Save image with RxJava2
     *
     * @param bitmap Bitmap for saving
     * @return Single of saving image
     */
    public Single<Uri> saveAsSingle(final PixelMap bitmap) {
        return Single.fromCallable(new Callable<Uri>() {

            @Override
            public Uri call() throws Exception {
                return saveImage(System.currentTimeMillis() + "", bitmap);
            }
        }).doOnSubscribe(new Consumer<Disposable>() {
            @Override
            public void accept(@NonNull Disposable disposable) throws Exception {
                mIsSaving.set(true);
            }
        }).doFinally(new Action() {
            @Override
            public void run() throws Exception {
                mIsSaving.set(false);
            }
        });
    }

    /**
     * Save image with Builder Pattern
     *
     * @param bitmap image for saving
     * @return Builder
     */
    public SaveRequest save(PixelMap bitmap) {
        return new SaveRequest(this, bitmap);
    }

    private PixelMap cropImage() throws IOException, IllegalStateException {
        PixelMap cropped;

        if (mSourceUri == null) {
            cropped = getCroppedBitmap();
        } else {
            cropped = getCroppedBitmapFromUri();
            if (mCropMode == CropMode.CIRCLE) {
                PixelMap circle = getCircularBitmap(cropped);
                if (cropped != getPixelMap()) {
                    cropped.release();
                }
                cropped = circle;
            }
        }

        cropped = scaleBitmapIfNeeded(cropped);

        mOutputImageWidth = cropped.getImageInfo().size.width;
        mOutputImageHeight = cropped.getImageInfo().size.height;

        return cropped;
    }

    /**
     * Get frame position relative to the source bitmap.
     *
     * @return getCroppedBitmap area boundaries.
     */
    public RectFloat getActualCropRect() {
        if (mImageRect == null) return null;
        float offsetX = (mImageRect.left / mScale);
        float offsetY = (mImageRect.top / mScale);
        float l = (mFrameRect.left / mScale) - offsetX;
        float t = (mFrameRect.top / mScale) - offsetY;
        float r = (mFrameRect.right / mScale) - offsetX;
        float b = (mFrameRect.bottom / mScale) - offsetY;
        l = Math.max(0, l);
        t = Math.max(0, t);
        r = Math.min(mImageRect.right / mScale, r);
        b = Math.min(mImageRect.bottom / mScale, b);
        return new RectFloat(l, t, r, b);
    }

    private RectFloat applyInitialFrameRect(RectFloat initialFrameRect) {
        RectFloat frameRect = new RectFloat();
        frameRect.left = initialFrameRect.left * mScale;
        frameRect.top = initialFrameRect.top * mScale;
        frameRect.right = initialFrameRect.right * mScale;
        frameRect.bottom = initialFrameRect.bottom * mScale;
        frameRect.left += mImageRect.left;
        frameRect.right += mImageRect.right;
        frameRect.top += mImageRect.top;
        frameRect.bottom += mImageRect.bottom;
        float l = Math.max(mImageRect.left, frameRect.left);
        float t = Math.max(mImageRect.top, frameRect.top);
        float r = Math.min(mImageRect.right, frameRect.right);
        float b = Math.min(mImageRect.bottom, frameRect.bottom);
        frameRect.left = l;
        frameRect.top = t;
        frameRect.right = r;
        frameRect.bottom = b;
        return frameRect;
    }

    /**
     * Set getCroppedBitmap mode
     *
     * @param mode           getCroppedBitmap mode
     * @param durationMillis animation duration in milliseconds
     */
    public void setCropMode(CropMode mode, int durationMillis) {
        if (mode == CropMode.CUSTOM) {
            setCustomRatio(1, 1);
        } else {
            mCropMode = mode;
            recalculateFrameRect(durationMillis);
        }
    }

    /**
     * Set getCroppedBitmap mode
     *
     * @param mode getCroppedBitmap mode
     */
    public void setCropMode(CropMode mode) {
        setCropMode(mode, mAnimationDurationMillis);
    }

    public CropMode getCropMode() {
        return mCropMode;
    }

    /**
     * Set custom aspect ratio to getCroppedBitmap frame
     *
     * @param ratioX         ratio x
     * @param ratioY         ratio y
     * @param durationMillis animation duration in milliseconds
     */
    public void setCustomRatio(int ratioX, int ratioY, int durationMillis) {
        if (ratioX == 0 || ratioY == 0) return;
        mCropMode = CropMode.CUSTOM;
        mCustomRatio = new Point(ratioX, ratioY);
        recalculateFrameRect(durationMillis);
    }

    public Point getCustomRatio() {
        return mCustomRatio;
    }

    /**
     * Set custom aspect ratio to getCroppedBitmap frame
     *
     * @param ratioX ratio x
     * @param ratioY ratio y
     */
    public void setCustomRatio(int ratioX, int ratioY) {
        setCustomRatio(ratioX, ratioY, mAnimationDurationMillis);
    }

    /**
     * Set image overlay color
     *
     * @param overlayColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setOverlayColor(int overlayColor) {
        this.mOverlayColor = overlayColor;
        invalidate();
    }

    public int getOverlayColor() {
        return mOverlayColor;
    }

    /**
     * Set getCroppedBitmap frame color
     *
     * @param frameColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setFrameColor(int frameColor) {
        this.mFrameColor = frameColor;
        invalidate();
    }

    public int getFrameColor() {
        return mFrameColor;
    }

    /**
     * Set handle color
     *
     * @param handleColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setHandleColor(int handleColor) {
        this.mHandleColor = handleColor;
        invalidate();
    }

    public int getHandleColor() {
        return mHandleColor;
    }

    /**
     * Set guide color
     *
     * @param guideColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setGuideColor(int guideColor) {
        this.mGuideColor = guideColor;
        invalidate();
    }

    public int getGuideColor() {
        return mGuideColor;
    }

    /**
     * Set view background color
     *
     * @param bgColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setBackgroundColor(int bgColor) {
        this.mBackgroundColor = bgColor;
        invalidate();
    }

    public int getBackgroundColor() {
        return mBackgroundColor;
    }

    /**
     * Set getCroppedBitmap frame minimum size in density-independent pixels.
     *
     * @param minDp getCroppedBitmap frame minimum size in density-independent pixels
     */
    public void setMinFrameSizeInDp(int minDp) {
        mMinFrameSize = minDp * getDensity();
    }

    public float getMineFrameSize() {
        return mMinFrameSize;
    }

    /**
     * Set getCroppedBitmap frame minimum size in pixels.
     *
     * @param minPx getCroppedBitmap frame minimum size in pixels
     */
    public void setMinFrameSizeInPx(int minPx) {
        mMinFrameSize = minPx;
    }

    /**
     * Set handle radius in density-independent pixels.
     *
     * @param handleDp handle radius in density-independent pixels
     */
    public void setHandleSizeInDp(int handleDp) {
        mHandleSize = (int) (handleDp * getDensity());
    }

    public int getHandleSize() {
        return mHandleSize;
    }

    /**
     * Set getCroppedBitmap frame handle touch padding(touch area) in density-independent pixels.
     * <p>
     * handle touch area : a circle of radius R.(R = handle size + touch padding)
     *
     * @param paddingDp getCroppedBitmap frame handle touch padding(touch area) in
     *                  density-independent
     *                  pixels
     */
    public void setTouchPaddingInDp(int paddingDp) {
        mTouchPadding = (int) (paddingDp * getDensity());
    }

    public int getTouchPadding() {
        return mTouchPadding;
    }

    /**
     * Set guideline show mode.
     * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH)
     *
     * @param mode guideline show mode
     */
    public void setGuideShowMode(ShowMode mode) {
        mGuideShowMode = mode;
        switch (mode) {
            case SHOW_ALWAYS:
                mShowGuide = true;
                break;
            case NOT_SHOW:
            case SHOW_ON_TOUCH:
                mShowGuide = false;
                break;
        }
        invalidate();
    }

    public ShowMode getGuideShowMode() {
        return mGuideShowMode;
    }

    /**
     * Set handle show mode.
     * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH)
     *
     * @param mode handle show mode
     */
    public void setHandleShowMode(ShowMode mode) {
        mHandleShowMode = mode;
        switch (mode) {
            case SHOW_ALWAYS:
                mShowHandle = true;
                break;
            case NOT_SHOW:
            case SHOW_ON_TOUCH:
                mShowHandle = false;
                break;
        }
        invalidate();
    }

    public ShowMode getHandleShowMode() {
        return mHandleShowMode;
    }

    /**
     * Set frame stroke weight in density-independent pixels.
     *
     * @param weightDp frame stroke weight in density-independent pixels.
     */
    public void setFrameStrokeWeightInDp(int weightDp) {
        mFrameStrokeWeight = weightDp * getDensity();
        invalidate();
    }

    public float getFrameStrokeWeight() {
        return mFrameStrokeWeight;
    }

    /**
     * Set guideline stroke weight in density-independent pixels.
     *
     * @param weightDp guideline stroke weight in density-independent pixels.
     */
    public void setGuideStrokeWeightInDp(int weightDp) {
        mGuideStrokeWeight = weightDp * getDensity();
        invalidate();
    }

    public float getGuideStrokeWeight() {
        return mGuideStrokeWeight;
    }

    /**
     * Set whether to show getCroppedBitmap frame.
     *
     * @param enabled should show getCroppedBitmap frame?
     */
    public void setCropEnabled(boolean enabled) {
        mIsCropEnabled = enabled;
        invalidate();
    }

    public boolean isCropEnabled() {
        return mIsCropEnabled;
    }

    /**
     * Set locking the getCroppedBitmap frame.
     *
     * @param enabled should lock getCroppedBitmap frame?
     */
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        mIsEnabled = enabled;
    }

    /**
     * Set initial scale of the frame.(0.01 ~ 1.0)
     *
     * @param initialScale initial scale
     */
    public void setInitialFrameScale(float initialScale) {
        mInitialFrameScale = constrain(initialScale, 0.01f, 1.0f, DEFAULT_INITIAL_FRAME_SCALE);
    }

    /**
     * Set whether to animate
     *
     * @param enabled is animation enabled?
     */
    public void setAnimationEnabled(boolean enabled) {
        mIsAnimationEnabled = enabled;
    }

    public boolean isAnimationEnabled() {
        return mIsAnimationEnabled;
    }

    /**
     * Set duration of animation
     *
     * @param durationMillis animation duration in milliseconds
     */
    public void setAnimationDuration(int durationMillis) {
        mAnimationDurationMillis = durationMillis;
    }

    public long getAnimationDuration() {
        return mAnimationDurationMillis;
    }

    /**
     * Set interpolator of animation
     * (Default interpolator is DecelerateInterpolator)
     *
     * @param interpolator interpolator used for animation
     */
    public void setInterpolator(int interpolator) {
        mInterpolator = interpolator;
        mAnimator = null;
        setupAnimatorIfNeeded();
    }

    public int getInterpolator() {
        return mInterpolator;
    }

    /**
     * Set whether to show debug display
     *
     * @param debug is logging enabled
     */
    public void setDebug(boolean debug) {
        mIsDebug = debug;
        Logger.enabled = true;
        invalidate();
    }

    /**
     * Set whether to log exception
     *
     * @param enabled is logging enabled
     */
    public void setLoggingEnabled(boolean enabled) {
        Logger.enabled = enabled;
    }

    /**
     * Set fixed width for output
     * (After cropping, the image is scaled to the specified size.)
     *
     * @param outputWidth output width
     */
    public void setOutputWidth(int outputWidth) {
        mOutputWidth = outputWidth;
        mOutputHeight = 0;
    }

    /**
     * Set fixed height for output
     * (After cropping, the image is scaled to the specified size.)
     *
     * @param outputHeight output height
     */
    public void setOutputHeight(int outputHeight) {
        mOutputHeight = outputHeight;
        mOutputWidth = 0;
    }

    /**
     * Set maximum size for output
     * (If cropped image size is larger than max size, the image is scaled to the smaller size.
     * If fixed output width/height has already set, these parameters are ignored.)
     *
     * @param maxWidth  max output width
     * @param maxHeight max output height
     */
    public void setOutputMaxSize(int maxWidth, int maxHeight) {
        mOutputMaxWidth = maxWidth;
        mOutputMaxHeight = maxHeight;
    }

    /**
     * Set compress format for output
     *
     * @param format String
     */
    public void setCompressFormat(String format) {
        mCompressFormat = format;
    }

    /**
     * Set compress quality for output
     *
     * @param quality compress quality(0-100: 100 is default.)
     */
    public void setCompressQuality(int quality) {
        mCompressQuality = quality;
    }

    /**
     * Set whether to show handle shadows
     *
     * @param handleShadowEnabled should show handle shadows?
     */
    public void setHandleShadowEnabled(boolean handleShadowEnabled) {
        mIsHandleShadowEnabled = handleShadowEnabled;
    }

    /**
     * cropping status
     *
     * @return is cropping process running
     */
    public boolean isCropping() {
        return mIsCropping.get();
    }

    /**
     * source uri
     *
     * @return source uri
     */
    public Uri getSourceUri() {
        return mSourceUri;
    }

    /**
     * save uri
     *
     * @return save uri
     */
    public Uri getSaveUri() {
        return mSaveUri;
    }

    /**
     * saving status
     *
     * @return is saving process running
     */
    public boolean isSaving() {
        return mIsSaving.get();
    }

    private void setScale(float mScale) {
        this.mScale = mScale;
    }

    private void setCenter(Point mCenter) {
        this.mCenter = mCenter;
    }

    private float getFrameW() {
        return (mFrameRect.right - mFrameRect.left);
    }

    private float getFrameH() {
        return (mFrameRect.bottom - mFrameRect.top);
    }


    @Override
    public void onComponentUnboundFromWindow(Component component) {

    }

    // Enum ////////////////////////////////////////////////////////////////////////////////////////

    private enum TouchArea {
        OUT_OF_BOUNDS, CENTER, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM, LEFT, TOP, RIGHT, BOTTOM
    }

    public enum CropMode {
        /**
         * FIT_IMAGE
         */
        FIT_IMAGE(0),
        /**
         * RATIO_4_3
         */
        RATIO_4_3(1),
        /**
         * RATIO_3_4
         */
        RATIO_3_4(2),
        /**
         * SQUARE
         */
        SQUARE(3),
        /**
         * RATIO_16_9
         */
        RATIO_16_9(4),
        /**
         * RATIO_9_16
         */
        RATIO_9_16(5),
        /**
         * FREE
         */
        FREE(6),
        /**
         * CUSTOM
         */
        CUSTOM(7),
        /**
         * CIRCLE
         */
        CIRCLE(8),
        /**
         * CIRCLE_SQUARE
         */
        CIRCLE_SQUARE(9);
        private final int ID;

        CropMode(final int id) {
            this.ID = id;
        }

        public int getId() {
            return ID;
        }
    }

    public enum ShowMode {
        /**
         * SHOW_ALWAYS
         */
        SHOW_ALWAYS(1),
        /**
         * SHOW_ON_TOUCH
         */
        SHOW_ON_TOUCH(2),
        /**
         * NOT_SHOW
         */
        NOT_SHOW(3);
        private final int ID;

        ShowMode(final int id) {
            this.ID = id;
        }

        public int getId() {
            return ID;
        }
    }

    public enum RotateDegrees {
        /**
         * 90D
         */
        ROTATE_90D(90),
        /**
         * 180D
         */
        ROTATE_180D(180),
        /**
         * 270D
         */
        ROTATE_270D(270),
        /**
         * -90
         */
        ROTATE_M90D(-90),
        /**
         * -180D
         */
        ROTATE_M180D(-180),
        /**
         * -270D
         */
        ROTATE_M270D(-270),
        /**
         * 0D
         */
        ROTATE_00D(0);

        private final int VALUE;

        RotateDegrees(final int value) {
            this.VALUE = value;
        }

        public int getValue() {
            return VALUE;
        }
    }


    public static class SavedState implements Serializable {
        CropMode mode;
        int backgroundColor;
        int overlayColor;
        int frameColor;
        ShowMode guideShowMode;
        ShowMode handleShowMode;
        boolean showGuide;
        boolean showHandle;
        int handleSize;
        int touchPadding;
        float minFrameSize;
        float customRatioX;
        float customRatioY;
        float frameStrokeWeight;
        float guideStrokeWeight;
        boolean isCropEnabled;
        int handleColor;
        int guideColor;
        float initialFrameScale;
        float angle;
        boolean isAnimationEnabled;
        int animationDuration;
        int exifRotation;
        Uri sourceUri;
        Uri saveUri;
        String format;
        int compressQuality;
        boolean isDebug;
        int outputMaxWidth;
        int outputMaxHeight;
        int outputWidth;
        int outputHeight;
        boolean isHandleShadowEnabled;
        int inputImageWidth;
        int inputImageHeight;
        int outputImageWidth;
        int outputImageHeight;
    }

}
